/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.tomcat.util.modeler.modules;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.List;

import javax.management.ObjectName;

import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.util.modeler.AttributeInfo;
import org.apache.tomcat.util.modeler.ManagedBean;
import org.apache.tomcat.util.modeler.OperationInfo;
import org.apache.tomcat.util.modeler.ParameterInfo;
import org.apache.tomcat.util.modeler.Registry;

public class MbeansDescriptorsIntrospectionSource extends ModelerSource {
	private static Log log = LogFactory
			.getLog(MbeansDescriptorsIntrospectionSource.class);

	Registry registry;
	String location;
	String type;
	Object source;
	List mbeans = new ArrayList();

	public void setRegistry(Registry reg) {
		this.registry = reg;
	}

	public void setLocation(String loc) {
		this.location = loc;
	}

	/**
	 * Used if a single component is loaded
	 * 
	 * @param type
	 */
	public void setType(String type) {
		this.type = type;
	}

	public void setSource(Object source) {
		this.source = source;
	}

	public List loadDescriptors(Registry registry, String location,
			String type, Object source) throws Exception {
		setRegistry(registry);
		setLocation(location);
		setType(type);
		setSource(source);
		execute();
		return mbeans;
	}

	public void execute() throws Exception {
		if (registry == null)
			registry = Registry.getRegistry();
		try {
			ManagedBean managed = createManagedBean(registry, null,
					(Class) source, type);
			if (managed == null)
				return;
			managed.setName(type);

			mbeans.add(managed);

		} catch (Exception ex) {
			log.error("Error reading descriptors ", ex);
		}
	}

	// ------------ Implementation for non-declared introspection classes

	static Hashtable specialMethods = new Hashtable();
	static {
		specialMethods.put("preDeregister", "");
		specialMethods.put("postDeregister", "");
	}

	private static String strArray[] = new String[0];
	private static ObjectName objNameArray[] = new ObjectName[0];
	// createMBean == registerClass + registerMBean

	private static Class[] supportedTypes = new Class[] { Boolean.class,
			Boolean.TYPE, Byte.class, Byte.TYPE, Character.class,
			Character.TYPE, Short.class, Short.TYPE, Integer.class,
			Integer.TYPE, Long.class, Long.TYPE, Float.class, Float.TYPE,
			Double.class, Double.TYPE, String.class, strArray.getClass(),
			BigDecimal.class, BigInteger.class, ObjectName.class,
			objNameArray.getClass(), java.io.File.class, };

	/**
	 * Check if this class is one of the supported types. If the class is
	 * supported, returns true. Otherwise, returns false.
	 * 
	 * @param ret
	 *            The class to check
	 * @return boolean True if class is supported
	 */
	private boolean supportedType(Class ret) {
		for (int i = 0; i < supportedTypes.length; i++) {
			if (ret == supportedTypes[i]) {
				return true;
			}
		}
		if (isBeanCompatible(ret)) {
			return true;
		}
		return false;
	}

	/**
	 * Check if this class conforms to JavaBeans specifications. If the class is
	 * conformant, returns true.
	 * 
	 * @param javaType
	 *            The class to check
	 * @return boolean True if the class is compatible.
	 */
	protected boolean isBeanCompatible(Class javaType) {
		// Must be a non-primitive and non array
		if (javaType.isArray() || javaType.isPrimitive()) {
			return false;
		}

		// Anything in the java or javax package that
		// does not have a defined mapping is excluded.
		if (javaType.getName().startsWith("java.")
				|| javaType.getName().startsWith("javax.")) {
			return false;
		}

		try {
			javaType.getConstructor(new Class[] {});
		} catch (java.lang.NoSuchMethodException e) {
			return false;
		}

		// Make sure superclass is compatible
		Class superClass = javaType.getSuperclass();
		if (superClass != null && superClass != java.lang.Object.class
				&& superClass != java.lang.Exception.class
				&& superClass != java.lang.Throwable.class) {
			if (!isBeanCompatible(superClass)) {
				return false;
			}
		}
		return true;
	}

	/**
	 * Process the methods and extract 'attributes', methods, etc
	 * 
	 * @param realClass
	 *            The class to process
	 * @param methods
	 *            The methods to process
	 * @param attMap
	 *            The attribute map (complete)
	 * @param getAttMap
	 *            The readable attributess map
	 * @param setAttMap
	 *            The settable attributes map
	 * @param invokeAttMap
	 *            The invokable attributes map
	 */
	private void initMethods(Class realClass, Method methods[],
			Hashtable attMap, Hashtable getAttMap, Hashtable setAttMap,
			Hashtable invokeAttMap) {
		for (int j = 0; j < methods.length; ++j) {
			String name = methods[j].getName();

			if (Modifier.isStatic(methods[j].getModifiers()))
				continue;
			if (!Modifier.isPublic(methods[j].getModifiers())) {
				if (log.isDebugEnabled())
					log.debug("Not public " + methods[j]);
				continue;
			}
			if (methods[j].getDeclaringClass() == Object.class)
				continue;
			Class params[] = methods[j].getParameterTypes();

			if (name.startsWith("get") && params.length == 0) {
				Class ret = methods[j].getReturnType();
				if (!supportedType(ret)) {
					if (log.isDebugEnabled())
						log.debug("Unsupported type " + methods[j]);
					continue;
				}
				name = unCapitalize(name.substring(3));

				getAttMap.put(name, methods[j]);
				// just a marker, we don't use the value
				attMap.put(name, methods[j]);
			} else if (name.startsWith("is") && params.length == 0) {
				Class ret = methods[j].getReturnType();
				if (Boolean.TYPE != ret) {
					if (log.isDebugEnabled())
						log.debug("Unsupported type " + methods[j] + " " + ret);
					continue;
				}
				name = unCapitalize(name.substring(2));

				getAttMap.put(name, methods[j]);
				// just a marker, we don't use the value
				attMap.put(name, methods[j]);

			} else if (name.startsWith("set") && params.length == 1) {
				if (!supportedType(params[0])) {
					if (log.isDebugEnabled())
						log.debug("Unsupported type " + methods[j] + " "
								+ params[0]);
					continue;
				}
				name = unCapitalize(name.substring(3));
				setAttMap.put(name, methods[j]);
				attMap.put(name, methods[j]);
			} else {
				if (params.length == 0) {
					if (specialMethods.get(methods[j].getName()) != null)
						continue;
					invokeAttMap.put(name, methods[j]);
				} else {
					boolean supported = true;
					for (int i = 0; i < params.length; i++) {
						if (!supportedType(params[i])) {
							supported = false;
							break;
						}
					}
					if (supported)
						invokeAttMap.put(name, methods[j]);
				}
			}
		}
	}

	/**
	 * XXX Find if the 'className' is the name of the MBean or the real class (
	 * I suppose first ) XXX Read (optional) descriptions from a .properties,
	 * generated from source XXX Deal with constructors
	 * 
	 * @param registry
	 *            The Bean registry (not used)
	 * @param domain
	 *            The bean domain (not used)
	 * @param realClass
	 *            The class to analyze
	 * @param type
	 *            The bean type
	 * @return ManagedBean The create MBean
	 */
	public ManagedBean createManagedBean(Registry registry, String domain,
			Class realClass, String type) {
		ManagedBean mbean = new ManagedBean();

		Method methods[] = null;

		Hashtable attMap = new Hashtable();
		// key: attribute val: getter method
		Hashtable getAttMap = new Hashtable();
		// key: attribute val: setter method
		Hashtable setAttMap = new Hashtable();
		// key: operation val: invoke method
		Hashtable invokeAttMap = new Hashtable();

		methods = realClass.getMethods();

		initMethods(realClass, methods, attMap, getAttMap, setAttMap,
				invokeAttMap);

		try {

			Enumeration en = attMap.keys();
			while (en.hasMoreElements()) {
				String name = (String) en.nextElement();
				AttributeInfo ai = new AttributeInfo();
				ai.setName(name);
				Method gm = (Method) getAttMap.get(name);
				if (gm != null) {
					// ai.setGetMethodObj( gm );
					ai.setGetMethod(gm.getName());
					Class t = gm.getReturnType();
					if (t != null)
						ai.setType(t.getName());
				}
				Method sm = (Method) setAttMap.get(name);
				if (sm != null) {
					// ai.setSetMethodObj(sm);
					Class t = sm.getParameterTypes()[0];
					if (t != null)
						ai.setType(t.getName());
					ai.setSetMethod(sm.getName());
				}
				ai.setDescription("Introspected attribute " + name);
				if (log.isDebugEnabled())
					log.debug("Introspected attribute " + name + " " + gm + " "
							+ sm);
				if (gm == null)
					ai.setReadable(false);
				if (sm == null)
					ai.setWriteable(false);
				if (sm != null || gm != null)
					mbean.addAttribute(ai);
			}

			en = invokeAttMap.keys();
			while (en.hasMoreElements()) {
				String name = (String) en.nextElement();
				Method m = (Method) invokeAttMap.get(name);
				if (m != null && name != null) {
					OperationInfo op = new OperationInfo();
					op.setName(name);
					op.setReturnType(m.getReturnType().getName());
					op.setDescription("Introspected operation " + name);
					Class parms[] = m.getParameterTypes();
					for (int i = 0; i < parms.length; i++) {
						ParameterInfo pi = new ParameterInfo();
						pi.setType(parms[i].getName());
						pi.setName("param" + i);
						pi.setDescription("Introspected parameter param" + i);
						op.addParameter(pi);
					}
					mbean.addOperation(op);
				} else {
					log.error("Null arg " + name + " " + m);
				}
			}

			/*
			 * Constructor[] constructors = realClass.getConstructors(); for(int
			 * i=0;i<constructors.length;i++) { ConstructorInfo info = new
			 * ConstructorInfo(); String className = realClass.getName(); int
			 * nIndex = -1; if((nIndex = className.lastIndexOf('.'))!=-1) {
			 * className = className.substring(nIndex+1); }
			 * info.setName(className);
			 * info.setDescription(constructors[i].getName()); Class classes[] =
			 * constructors[i].getParameterTypes(); for(int
			 * j=0;j<classes.length;j++) { ParameterInfo pi = new
			 * ParameterInfo(); pi.setType(classes[j].getName());
			 * pi.setName("param" + j);
			 * pi.setDescription("Introspected parameter param" + j);
			 * info.addParameter(pi); } mbean.addConstructor(info); }
			 */

			if (log.isDebugEnabled())
				log.debug("Setting name: " + type);
			mbean.setName(type);

			return mbean;
		} catch (Exception ex) {
			ex.printStackTrace();
			return null;
		}
	}

	// -------------------- Utils --------------------
	/**
	 * Converts the first character of the given String into lower-case.
	 * 
	 * @param name
	 *            The string to convert
	 * @return String
	 */
	private static String unCapitalize(String name) {
		if (name == null || name.length() == 0) {
			return name;
		}
		char chars[] = name.toCharArray();
		chars[0] = Character.toLowerCase(chars[0]);
		return new String(chars);
	}

}

// End of class: MbeanDescriptorsIntrospectionSource
